----------------------------------------
--
-- Core module (based on Faceroller.lua by hungtar)
--
local ADDON_NAME, ns = ...

local _G = _G

local VERSION = {string.split('.', _G.GetAddOnMetadata('Rotor', 'Version'))}
local VERSION_NUMBER = tonumber(VERSION[1]) * 100 * 100 + tonumber(VERSION[2]) * 100 + tonumber(VERSION[3])

----------------------------------------
--
-- default debug flags
--
local DEBUG = false
local TRACE_EVENTS = false

----------------------------------------
--
-- Stub for localization
--
ns.L = setmetatable({}, {
    __index = function(t, v)
        t[v] = v
        return v
    end
})
local L = ns.L

----------------------------------------
--
-- Configuration
--
local default_config = {
    version_number = VERSION_NUMBER,

    locked = false,

    time_to_wait_of_send_after_gcd = 0.5,

    framescale = 1.0,
    framealpha = 1.0,
    x = 0,
    y = -90,

    showinraid = true,
    showinparty = true,
    showinpvp = true,
    showwhensolo = true,
    showinvehicles = false,

    boss_only = false,

    silent = true,

    opts = {},

    backdropcolor = {0.14, 0.14, 0.14, 0.75},
    bordercolor = {0.3, 0.3, 0.3, 0.75},
    icon_shade_color = {0.3, 0.3, 0.3},
    oor_icon_shade_color = {0.7, 0, 0},

    main_icon_size = 40,
    icon_spacing = 1,

    font = [[Fonts\ArialN.ttf]],
    condensed_font = [[Interface\AddOns\rotor\media\myrbc.ttf]],
    mono_font = [[Interface\AddOns\rotor\media\LiberationMono-Regular.ttf]],
    font_size = 14
}
----------------------------------------
_G.RotorConfigDB = {
    version_number = VERSION_NUMBER,
}
----------------------------------------
ns.cfg = setmetatable({}, {
    __index = function(t, k)
        local v = _G.RotorConfigDB[k]
        if v == nil then
            return default_config[k]
        else
            return v
        end
    end,
    __newindex = function(t, k, v)
        if v == default_config[k] then
            _G.RotorConfigDB[k] = nil -- reset to default
        else
            _G.RotorConfigDB[k] = v
        end
        return v
    end}
)
----------------------------------------
local function createOptTable(table_key)
    return setmetatable(
        {
            table_key = table_key,
            --storage -- table of options
        },
        {
            __index = function(self, k)
                local storage = rawget(self, 'storage')
                if not storage then
                    return nil
                else
                    return storage[k]
                end
            end,
            __newindex = function(self, k, v)
                local storage = rawget(self, 'storage')
                if v == nil then
                    if storage then
                        storage[k] = nil
                        if not next(storage) then
                            _G.RotorConfigDB.opts[rawget(self, 'table_key')] = nil -- no more custom options
                            if not next(_G.RotorConfigDB.opts) then
                                _G.RotorConfigDB.opts = nil -- no more custom tables
                            end
                        end
                    end
                else
                    if not storage then
                        local opts
                        if not _G.RotorConfigDB.opts then
                            opts = {}
                            _G.RotorConfigDB.opts = opts
                        end
                        storage = opts[rawget(self, 'table_key')]
                        if not storage then
                            storage = {}
                            opts[rawget(self, 'table_key')] = storage
                            rawset(self, 'storage', storage)
                        end
                    end
                    storage[k] = v
                end
                return v
            end
        })
end

----------------------------------------
--
-- Local references to globals for speed and lint's happiness
--
local GetTime = _G.GetTime
local GetSpellCooldown = _G.GetSpellCooldown
local GetSpellInfo = _G.GetSpellInfo
local UnitAura = _G.UnitAura
local pairs = _G.pairs
local type = _G.type
local select = _G.select
local tinsert = _G.tinsert
local tremove = _G.tremove
local IsSpellInRange = _G.IsSpellInRange
local UnitPower = _G.UnitPower
local GetComboPoints = _G.GetComboPoints
local UnitGUID = _G.UnitGUID
local strsplit = _G.strsplit
local UIParent = _G.UIParent
local CreateFrame = _G.CreateFrame
local UnitName = _G.UnitName
local UnitCanAttack = _G.UnitCanAttack
local UnitIsDead = _G.UnitIsDead
local UnitInVehicle = _G.UnitInVehicle
local DEFAULT_CHAT_FRAME = _G.DEFAULT_CHAT_FRAME
local UnitClass = _G.UnitClass
local GetActiveTalentGroup = _G.GetActiveTalentGroup
local GetPrimaryTalentTree = _G.GetPrimaryTalentTree
local GetSpellTabInfo = _G.GetSpellTabInfo
local GetSpellBookItemName = _G.GetSpellBookItemName
local BOOKTYPE_SPELL = _G.BOOKTYPE_SPELL
local NUM_GLYPH_SLOTS = _G.NUM_GLYPH_SLOTS
local GetGlyphSocketInfo = _G.GetGlyphSocketInfo
local GetInventoryItemLink = _G.GetInventoryItemLink
local GetNumRaidMembers = _G.GetNumRaidMembers
local GetNumPartyMembers = _G.GetNumPartyMembers
local GetInstanceInfo = _G.GetInstanceInfo
local SlashCmdList = _G.SlashCmdList
local format = _G.format
local UnitLevel = _G.UnitLevel
local UnitCastingInfo = _G.UnitCastingInfo
local UnitIsPlayer = _G.UnitIsPlayer
local UnitHealthMax = _G.UnitHealthMax
local SPELL_POWER_HOLY_POWER = _G.SPELL_POWER_HOLY_POWER
local IsSpellKnown = _G.IsSpellKnown
local GetNumTalents = _G.GetNumTalents
local GetTalentInfo = _G.GetTalentInfo
local UnitExists = _G.UnitExists
local IsUsableSpell = _G.IsUsableSpell
local GetRuneType = _G.GetRuneType
local GetRuneCooldown = _G.GetRuneCooldown
local UnitChannelInfo = _G.UnitChannelInfo
local UnitHealth = _G.UnitHealth
local debugprofilestop = _G.debugprofilestop
local PlaySoundFile = _G.PlaySoundFile

----------------------------------------
--
-- The Rotor
--
local Rotor = {
    --
    -- Public fields
    --
    version = VERSION,

    visible = false,
    on_target = false,
    in_combat = false,
    power = 0,
    combo_points = 0,
    pet_exists = false,
    pet_alive = false,

    target_guid = nil,
    target_is_player = false,
    target_level = 0,

    player_class = select(2, UnitClass('player')),
    spec = 0,
    player_level = 0,
    talents = {}, -- table of talent name -> talent rank
    talents_sig = '',
    glyphs = {}, -- table of glyph id -> true

    end_of_gcd = 0,
    spellcast_start_time = 0,
    spellcast_end_time = 0,

    default_texture = 'Interface\\Icons\\INV_Misc_PocketWatch_01',

    track_interruptible_target_spells = false,
    target_casting_interruptible_spell = false,

    runes = {
        -- cooldown and types
        {cd = 0, rt = 0},   -- blood 1
        {cd = 0, rt = 0},   -- blood 2
        {cd = 0, rt = 0},   -- unholy 1
        {cd = 0, rt = 0},   -- unholy 2
        {cd = 0, rt = 0},   -- frost 1
        {cd = 0, rt = 0},   -- frost 2
        counts = {0, 0, 0, 0},
        rune_counts_sig = 0,
    },

    --
    -- Internal config
    --
    update_frequency = 0.2,
    next_skill_check_interval = 0.2,

    -- Debugging
    debug = DEBUG,
    trace_events = TRACE_EVENTS,
    profile_start_time = 0, --millis with precision, returned by debugprofilestop
    total_profile_time = 0,
    total_time_used = 0,
    time_used = 0,

    --
    -- Private fields
    --
    modules = {},
    plugins = nil,

    spells = {},
    icons = {},
    auras = {},
    gcd_spell = 0,
    cur_mod = nil, -- currently selected module
    player_in_raid = false,
    player_in_party = false,
    player_in_pvp = false,
    -- only enable this for classes that might need them
    power_update = false,
    combo_update = false,
    runes_update = false,

    -- ttl
    cur_dps = 0,
    last_hp_probe_time = 0,
    hp_probes = ns.newQueue(10),
    hp_sample_rate = 0.5, -- in seconds
}
_G.Rotor = Rotor

local MAX_COMBO_SOUND = [[Interface\AddOns\rotor\media\max_combo.mp3]]

local gcd = 0    -- gcd at update moment
local now = 0    -- time at update moment

----------------------------------------
--
-- Define caches
--
Rotor.spell_icon_cache = setmetatable({}, {
    __index = function(t, spell)
        local name, _, tex = GetSpellInfo(spell)
        if tex then
            t[spell] = tex
        end
        if name and type(spell) == 'number' then
            Rotor.spell_name_cache[spell] = name
            Rotor.spell_id_cache[name] = spell
        end

        return tex
    end
})
----------------------------------------
Rotor.spell_name_cache = setmetatable({}, {
    __index = function(t, spell_id)
        local name, _, tex = GetSpellInfo(spell_id)
        if name then
            t[spell_id] = name
            if tex then
                Rotor.spell_icon_cache[spell_id] = tex
            end
            Rotor.spell_id_cache[name] = spell_id
        end
        return name
    end
})
----------------------------------------
Rotor.spell_id_cache = setmetatable({}, {
    __index = function(t, spell_name)
        for id, spell in pairs(Rotor.spells) do
            if spell.name == spell_name then
                t[spell_name] = id
                return id
            end
        end

        t[spell_name] = -1
        return -1
    end
})

----------------------------------------
--
-- Debugging
--
local log_time_base
local function dprint(...)
    if not log_time_base then
        log_time_base = GetTime()
    end
    local prefix = format('RDB %.3f:', GetTime() - log_time_base)
    print(prefix, ...)
end

function Rotor:dprint(...)
    if self.debug then
        dprint(...)
    end
end

----------------------------------------
--
-- Printing
--
function Rotor:print(x)
    DEFAULT_CHAT_FRAME:AddMessage('|cFFFF0000Rotor:|r ' .. x)
end
----------------------------------------
function Rotor:printOptInfo(x)
    if not self.cfg.silent then
        self:print(x)
    end
end

----------------------------------------
--
-- Event handler
--
local EventHandler = CreateFrame('Frame')
function EventHandler:onUpdate(elapsed)
    Rotor:update(elapsed)
end
function EventHandler:onEvent(event, ...)
    if Rotor.trace_events then
        dprint(event, ...)
    end
    Rotor[event](Rotor, ...)
end
EventHandler:SetScript('OnEvent', EventHandler.onEvent)
EventHandler:SetScript('OnUpdate', EventHandler.onUpdate)

----------------------------------------
--
-- Custom events support
--
local CustomEventHandler = CreateFrame('Frame')
local handlers_by_event = {}
local event_handler_counts = {}
function Rotor:addEventHandler(event, handler)
    if not handlers_by_event[event] then
        handlers_by_event[event] = {handler}
    else
        local handlers = handlers_by_event[event]
        for _, v in ipairs(handlers) do
            if v == handler then
                return -- already registered
            end
        end
        tinsert(handlers, handler)
    end
    local handler_count = (event_handler_counts[event] or 0) + 1
    event_handler_counts[event] = handler_count
    if handler_count == 1 then
        CustomEventHandler:RegisterEvent(event)
    end
end
----------------------------------------
function Rotor:removeEventHandler(event, handler)
    local handlers = handlers_by_event[event]
    assert(handlers, 'no handlers for event '..event)
    local ix
    for i, h in ipairs(handlers) do
        if h == handler then
            ix = i
            break
        end
    end
    assert(ix, 'handler not found')
    tremove(handlers, ix)
    local handler_count = event_handler_counts[event] - 1
    event_handler_counts[event] = handler_count
    if handler_count == 0 then
        CustomEventHandler:UnregisterEvent(event)
    end
end
----------------------------------------
function CustomEventHandler:onEvent(event, ...)
    local handlers = handlers_by_event[event]
    for _, handler in ipairs(handlers) do
        handler[event](handler, ...)
    end
end
CustomEventHandler:SetScript('OnEvent', CustomEventHandler.onEvent)

----------------------------------------
--
-- Event registration/unregistration
--
function Rotor:registerEvent(event)
    EventHandler:RegisterEvent(event)
end
----------------------------------------
function Rotor:unregisterEvent(event)
    EventHandler:UnregisterEvent(event)
end

----------------------------------------
--
-- Utilites
--
local function timeval(t)
    if t < 0.01 then
        return 0
    else
        return t
    end
end
----------------------------------------
local function getTalents()
    local sig = ''
    local ttbl = {}

    for tab = 1, 3 do
        local numTalents = GetNumTalents(tab)
        local talent = 0
        for talent = 1, numTalents do
            local talent_name, _, _, _, current_rank, _, _, _ = GetTalentInfo(tab, talent)
            ttbl[talent_name] = current_rank
            sig = sig..tostring(current_rank)
        end
    end
    return ttbl, sig
end
----------------------------------------
local function getGlyphs()
    local glyphs = {}
    local talentGroup = GetActiveTalentGroup()

    for i = 1, NUM_GLYPH_SLOTS do
        local enabled, _, _, glyph_id = GetGlyphSocketInfo(i, talentGroup)
        if enabled and glyph_id then
            glyphs[glyph_id] = true
        end
    end
    return glyphs
end
----------------------------------------
local BL_BUFF_IDS = {
    32182,  -- heroism
    2825,   -- blood lust
    80353,  -- time warp
    90355   -- ancient hysteria
}
function Rotor:initBlIndicator(icon_pos)
    for _, bl_id in pairs(BL_BUFF_IDS) do
        self:RegisterBuff(bl_id, true, icon_pos)
    end
end

----------------------------------------
--
-- Initialization
--
function Rotor:initConfig()
    self.cfg = ns.cfg

    if (self.cfg.version_number or 0) < VERSION_NUMBER then
        self:updateConfigToCurrentVersion()
    end

    -- check remove obsolete params
    local keys_to_remove = {}
    for k in pairs(_G.RotorConfigDB) do
        if default_config[k] == nil then
            keys_to_remove[k] = true
        end
    end
    for k in pairs(keys_to_remove) do
        _G.RotorConfigDB[k] = nil
    end
end
----------------------------------------
function Rotor:initialize()
    self:initConfig()
    self:initSlashCommands()

    if self.player_class == 'ROGUE' or self.player_class == 'DRUID' or self.player_class == 'PALADIN' then
        self.combo_update = true
        if self.player_class == 'PALADIN' then
            self.get_combo_points_func = function () return UnitPower('player', SPELL_POWER_HOLY_POWER) end
            self.max_combo_points = 3
        else
            self.get_combo_points_func = function() return GetComboPoints('player', 'target') end
            self.max_combo_points = 5
        end
    else
        self.combo_update = false
    end

    self.runes_update = self.player_class == 'DEATHKNIGHT'

    self.power_update = true

    self:createFrames('RotorDisplay')

    self:showOrHideAnchor()

    if self.plugins then
        for _, plugin in pairs(self.plugins) do
            plugin:Init(createOptTable(plugin:Name()))
        end
    end

    self:registerEvent('SKILL_LINES_CHANGED')
end
----------------------------------------
function Rotor:updateConfigToCurrentVersion()
    --
    -- do update here based on self.cfg.version_number
    --
    self.cfg.version_number = VERSION_NUMBER
end

----------------------------------------
--
-- Slash command functions
--
function Rotor:initSlashCommands()

    local slashCommands = {
        debug = Rotor.toggleDebug,
        version = Rotor.printVersion,
        lock = Rotor.toggleLock,
        scale = Rotor.setScaleOpt,
        alpha = Rotor.setAlphaOpt,
        list = Rotor.listModules,
        opt = Rotor.moduleOption,
        reset = Rotor.resetOptions,
        show = Rotor.setShowCondition,
        silent = Rotor.toggleSilent,
        te = Rotor.toggleEventTrace,
        x = Rotor.setX,
        y = Rotor.setY,
        pp = Rotor.printProfile,
    }

    SlashCmdList['ROTOR'] = function(cmd)
        local cmd, args = strsplit(' ', cmd:lower(), 2)
        if slashCommands[cmd] then
            return slashCommands[cmd](self, args)
        else
            self:printHelp()
        end
    end

    _G.SLASH_ROTOR1 = '/rotor'
    _G.SLASH_ROTOR2 = '/rr'
end
----------------------------------------
function Rotor:printHelp()
    self:print(L['Rotor options:'])
    self:print(L['version - print version number.'])
    self:print(L['lock - lock/unlock the frame.'])
    self:print(L['scale <num> - set scaling of the frame.'])
    self:print(L['alpha <num> - set alpha of the frame.'])
    self:print(L['list - print a list of available modules.'])
    self:print(L['opt <X> - pass X to the option handling function of the active module.'])
    self:print(L['show <X> - configure when to show Rotor, /rr show for help.'])
    self:print(L['range - toggle range check.'])
    self:print(L['power - toggle power check.'])
    self:print(L['reset [pos] - reset all options. if pos then reset also position'])
    self:print(L['silent - suppress some chat output.'])
    self:print(L['bossonly - toggle show only on boss targets.'])
end
----------------------------------------
function Rotor:printVersion()
    self:print(VERSION[1] .. '.' .. VERSION[2] .. '.' .. VERSION[3])
end
----------------------------------------
function Rotor:toggleDebug()
    if self.debug then
        self.debug = false
        self:print(L['debug disabled.'])
    else
        self.debug = true
        self:print(L['debug enabled.'])
    end
end
----------------------------------------
function Rotor:toggleEventTrace()
    self.trace_events = not self.trace_events
    if self.trace_events then
        self:print('Start event trace')
    else
        self:print('End event trace')
    end
end
----------------------------------------
function Rotor:toggleLock()
    self.cfg.locked = not self.cfg.locked
    self:showOrHideAnchor()
    if not self.cfg.locked then
        self:print(L['frame unlocked.'])
    else
        self.cfg.locked = true
        local _, _, _, x, y = self.anchor:GetPoint(0)
        self.cfg.x = x
        self.cfg.y = y
        self:print(L['frame locked.'])
    end
end
----------------------------------------
function Rotor:setScaleOpt(x)
    if x == nil then
        self:print(L['argument required.'])
        return
    end

    self.cfg.framescale = tonumber(x)
    self.anchor:SetScale(self.cfg.framescale)
    self.main_icon:SetScale(self.cfg.framescale)
    for _, icon in pairs(self.icons) do
        icon:SetScale(self.cfg.framescale)
    end
    self:pluginsDo(function(plugin) plugin:ScaleChanged(self.cfg.framescale) end)
end
----------------------------------------
function Rotor:listModules()
    self:print(L['available modules:'])
    for n, v in pairs(self.modules) do
        self:print(n)
    end
end
----------------------------------------
function Rotor:setModuleOption(x)
    if self.cur_mod:HandleCommandLine(x) then
        self:chooseModule()
    end
end
----------------------------------------
function Rotor:setAlphaOpt(x)
    if x == nil then
        self:print(L['argument required.'])
        return
    end

    self.cfg.framealpha = tonumber(x)
    self.main_icon:SetAlpha(self.cfg.framealpha)
    for _, icon in pairs(self.icons) do
        icon:SetAlpha(self.cfg.framealpha)
    end
    self:pluginsDo(function(plugin) plugin:AlphaChanged(self.cfg.framealpha) end)
end
----------------------------------------
function Rotor:resetOptions(x)
    local old_locked = self.cfg.locked

    _G.RotorConfigDB = {}

    if x ~= 'pos' then
        self.cfg.locked = old_locked
    end

    if x == 'pos' then
        self.cfg.locked = false
        self.anchor:SetPoint('CENTER', UIParent, 'CENTER', 0, 0)
        self.main_icon:SetScale(self.cfg.framescale)
        self.main_icon:SetAlpha(self.cfg.framealpha)

        self:showOrHideAnchor()
    end

    self:print(L['options have been reset.'])
end
----------------------------------------
local function toggle(opt_name, opt_descr)
    Rotor.cfg[opt_name] = not Rotor.cfg[opt_name]
    if Rotor.cfg[opt_name] then
        Rotor:print(L['Rotor will show ']..opt_descr..'.')
    end
end
----------------------------------------
local function printShowOpt(opt, descr)
    if opt then
        Rotor:print(' '..descr)
    end
end
----------------------------------------
function Rotor:setShowCondition(arg)
    if arg == 'raid' then
        toggle('showinraid', L['in raids'])
    elseif arg == 'party' then
        toggle('showinparty', L['in parties'])
    elseif arg == 'solo' then
        toggle('showwhensolo', L['when solo'])
    elseif arg == 'pvp' then
        toggle('showinpvp', L['in pvp'])
    elseif arg == 'vehicles' then
        toggle('showinvehicles', L['in vehicles'])
    elseif arg == 'bossonly' then
        toggle('boss_only', L['on boss targets only'])
    else
        self:print(L['Show Options:'])
        self:print(L[' show raid - hide/show in raids.'])
        self:print(L[' show party - hide/show in parties.'])
        self:print(L[' show solo - hide/show solo.'])
        self:print(L[' show pvp - hide/show in pvp.'])
        self:print(L[' show vehicles - hide/show in vehicles.'])
        self:print(L[' show bossonly - show on boss targets only.'])
        self:print(L['Rotor will currently show:'])
        printShowOpt(self.cfg.showinraid, L['in raids'])
        printShowOpt(self.cfg.showinparty, L['in party'])
        printShowOpt(self.cfg.showwhensolo, L['when solo'])
        printShowOpt(self.cfg.showinpvp, L['in pvp'])
        printShowOpt(self.cfg.showinvehicles, L['in vehicles'])
        printShowOpt(self.cfg.boss_only, L['only on bosses'])
    end
    self:checkTarget()
end
----------------------------------------
function Rotor:toggleBoolOpt(opt_name, descr)
    self.cfg[opt_name] = not self.cfg[opt_name]
    self:print(descr..' '..ns.iif(self.cfg[opt_name], L['enabled'], L['disabled'])..'.')
end
----------------------------------------
function Rotor:toggleSilent(x)
    self:toggleBoolOpt('silent', L['silent mode'])
end
----------------------------------------
function Rotor:setX(x)
    self.cfg.x = tonumber(x)
    self:applyPosition()
end
----------------------------------------
function Rotor:setY(y)
    self.cfg.y = tonumber(y)
    self:applyPosition()
end

----------------------------------------
--
-- Update function
--
local cur_spell = -1
local last_gcd = 0
local wait_for_send_till = 0
local last_update = 0
local last_cnsf = 0
local last_aura_scan_time = 0
local next_target_check = 0 -- time to check non-attackable target

----------------------------------------
function Rotor:forceUpdate(delay)
    last_cnsf = 0
    if delay == 0 then
        -- update immediately
        last_update = 0
        last_cnsf = 0
        self:update(0)
    else
        last_update = GetTime() + (delay or 0.01) - Rotor.update_frequency
    end
end
----------------------------------------
function Rotor:defaultRangeCheck(spell_id)
    return IsSpellInRange(self.spell_name_cache[spell_id]) ~= 0
end
----------------------------------------
function Rotor:defaultUsableCheck(spell_id)
    return IsUsableSpell(Rotor.spell_name_cache[spell_id]) == 1
end
----------------------------------------
function Rotor:checkCurSpellRangeAndUsability()
    local spell_usable, spell_in_range = true, true
    if cur_spell > 0 then
        spell_usable = self.cur_mod:CheckUsable(cur_spell)
        spell_in_range = self.cur_mod:CheckInRange(cur_spell)
    end

    if not spell_in_range then
        self.main_icon:shade(self.cfg.oor_icon_shade_color)
    elseif not spell_usable then
        self.main_icon:shade()
    else
        self.main_icon:unshade()
    end
end
----------------------------------------
function Rotor:updateSpells()
    for spell_id, spell in pairs(self.spells) do
        spell.cooldown_start, spell.cooldown_duration = GetSpellCooldown(spell_id)
        if spell.cooldown_start == 0 then
            spell.cooldown_duration = 0
        end
        spell.ends = spell.cooldown_start + spell.cooldown_duration
        spell.cd = timeval(spell.ends - now)
        spell.usable = self.cur_mod:CheckUsable(spell.id) or false
    end
end
----------------------------------------
function Rotor:updateGlobalCooldown()
    local start, duration = GetSpellCooldown(self.gcd_spell)
    if start and start > 0 then
        self.end_of_gcd = start + duration
        gcd = self.end_of_gcd - now
    else
        gcd = 0
    end
    if gcd < 0.001 then
        gcd = 0
        self.end_of_gcd = 0
        start = 0
        duration = 0
    end
    self.main_icon:setCooldown(start, duration)
end
----------------------------------------
function Rotor:updateRunes()
    self.runes.counts[1] = 0
    self.runes.counts[2] = 0
    self.runes.counts[3] = 0
    self.runes.counts[4] = 0
    for i = 1, 6 do
        local rt = GetRuneType(i)
        self.runes[i].rt = rt
        local s, d = GetRuneCooldown(i)
        if s == 0 then
            self.runes[i].cd = 0
        elseif s > now then
            self.runes[i].cd = d + (now - s)
        else
            self.runes[i].cd = s + d - now
            if self.runes[i].cd < 0.001 then
                self.runes[i].cd = 0
            end
        end
        if self.runes[i].cd == 0 then
            self.runes.counts[rt] = self.runes.counts[rt] + 1
        end
    end
    self.runes.rune_counts_sig =
        self.runes.counts[1] +
        self.runes.counts[2] * 10 +
        self.runes.counts[3] * 100 +
        self.runes.counts[4] * 1000
end
----------------------------------------
function Rotor:updateSpellsAurasAndResources()
    if self.power_update then
        local last_power = self.power
        self.power = UnitPower('player')
        if self.power_display_visible and (last_power ~= self.power) then
            self:updatePowerDisplay()
        end
    end

    if self.combo_update then
        local old_combo_points = self.combo_points
        self.combo_points = self.get_combo_points_func()
        if self.upper_text_visible then
            self.upper_text:SetText(self.combo_points)
        end
        if (old_combo_points ~= self.combo_points) and (self.combo_points == self.max_combo_points) then
            if (GetTime() - (self.max_combo_sound_play_time or 0)) > 2 then
                self.max_combo_sound_play_time = GetTime()
                PlaySoundFile(MAX_COMBO_SOUND, 'Master')
            end
        end
    end

    if self.runes_update then
        self:updateRunes()
    end

    self:checkPet()
    self:updateGlobalCooldown()
    if (GetTime() - last_aura_scan_time) < 3 then
        self:updateAuras()
    else
        -- do a full aura rescan each three seconds
        self:scanAuras()
    end
    self:updateSpells()

    if self.cur_mod then
        self.cur_mod:SpellsAurasAndResourcesUpdated()
    end

    if self.plugins then
        for _, plugin in pairs(self.plugins) do
            plugin:SpellsAurasAndResourcesUpdated()
        end
    end

    for _, icon in pairs(self.icons) do
        icon:update()
    end
end
----------------------------------------
function Rotor:update(elapsed)
    now = GetTime()

    if self.module_switch_time then
        if self.module_switch_time <= now then
            self.module_switch_time = nil
            self:chooseModule()
        end
        return
    end

    if self.plugins then
        for _, plugin in pairs(self.plugins) do
            plugin:update(elapsed)
        end
    end

    if (next_target_check ~= 0) and (now >= next_target_check) then
        self:checkTarget()
        return
    end

    if self.spellcast_end_time ~= 0 then
        if self.spellcast_end_time < now then
            self.spellcast_start_time = 0
            self.spellcast_end_time = 0
        end
    end

    self:updateBars(elapsed)

    if not self.visible then
        return
    end

    local startTime = debugprofilestop()

    if self.cur_mod then
        self.cur_mod:Update(elapsed)
    end

    if (now - self.last_hp_probe_time) >= self.hp_sample_rate then
        local hp = UnitHealth('target')
        self.last_hp_probe_time = now

        if self.hp_probes:is_full() then
            local thp = self.hp_probes:push(hp)
            self.cur_dps = (thp - hp) * self.hp_probes.count / self.hp_sample_rate
        else
            self.hp_probes:push(hp)
        end
    end

    if (now - last_update) < self.update_frequency then
        self:incTimeUsed(startTime)
        return
    end
    last_update = now

    self:updateSpellsAurasAndResources()

    if self.on_target then
        if (now - last_cnsf) > self.next_skill_check_interval then
            last_cnsf = now
            self:updateNextSkill()
        end

        self:checkCurSpellRangeAndUsability()
    end

    self:incTimeUsed(startTime)
end

----------------------------------------
--
-- call the active module nextskill function
--
function Rotor:updateNextSkill()
    local spell

    if gcd > last_gcd then
        -- some spell sent
        spell = 0 -- new spell required
        wait_for_send_till = now + gcd + (self.cur_mod:TimeToWaitOfSend() or self.cfg.time_to_wait_of_send_after_gcd)
    elseif now > wait_for_send_till then
        spell = 0 -- send wait complete, new spell required
    else
        spell = cur_spell
        if spell == -1 then
          spell = 0
        end
    end
    last_gcd = gcd

    spell = self.cur_mod:NextSkill(gcd, spell)

    if spell ~= cur_spell then
        cur_spell = spell
        if spell == 0 then
            self.main_icon:setTexture(nil)
        else
            self.main_icon:setTexture(self.spell_icon_cache[spell])
        end
    end
end

----------------------------------------
--
-- User interface functions
--
function Rotor:showOrHideAnchor()
    if self.cfg.locked then
        self.anchor:EnableMouse(false)
        self.anchor:Hide()
    else
        self.anchor:EnableMouse(true)
        self.anchor:Show()
    end
end
----------------------------------------
function Rotor:createOverlayText(name, vjustify)
    local text = self.main_icon_overlay:CreateFontString(name, 'OVERLAY')
    text:SetFont(self.cfg.font, self.cfg.font_size, 'THINOUTLINE')
    text:SetShadowColor(0, 0, 0, 0.4)
    text:SetShadowOffset(1, -1)
    text:SetWidth(self.main_icon_size - 2)
    text:SetHeight(math.floor(self.main_icon_size / 3))
    text:SetJustifyH('CENTER')
    text:SetJustifyV(vjustify or 'TOP')
    text:SetText('')
    text:Hide()
    return text
end
----------------------------------------
function Rotor:createFrames(name)
    self:initIcons()
    -- anchor for spell frame
    local anchor = CreateFrame('Frame', name .. '_Anchor', UIParent)
    anchor:RegisterForDrag('LeftButton')
    local anchor_size = self.main_icon_size + self.icon_size * 2 + self.cfg.icon_spacing * 2 + 4
    anchor:SetWidth(anchor_size)
    anchor:SetHeight(anchor_size)
    anchor:SetMovable(true)
    anchor:EnableMouse(true)
    anchor:ClearAllPoints()
    anchor:SetPoint('CENTER', UIParent, 'CENTER', 0, 0)
    anchor.bg = anchor:CreateTexture(name .. '_AnchorBG', 'PARENT')
    anchor.bg:SetAllPoints(anchor)
    anchor.bg:SetBlendMode('BLEND')
    anchor.bg:SetTexture(0, 0, 0, 0.4)
    anchor:SetScript('OnDragStart', EventHandler.StartMoving)
    anchor:SetScript('OnDragStop', EventHandler.StopMovingOrSizing)
    anchor:SetScale(self.cfg.framescale)
    self.anchor = anchor

    -- spell frame
    self.main_icon = self:createIcon(name .. 'MainIcon')
    self.main_icon:SetPoint('CENTER', anchor, 'CENTER')

    self.main_icon_overlay = CreateFrame('Frame', 'RotorMainIconOverlay', self.main_icon)
    self.main_icon_overlay:SetAllPoints(self.main_icon)
    self.main_icon_overlay:SetFrameLevel(self.main_icon.cd:GetFrameLevel() + 1)

    self.power_text = self:createOverlayText(name..'PowerText', 'BOTTOM')
    self.power_text:ClearAllPoints()
    self.power_text:SetPoint('BOTTOMLEFT', self.main_icon, 'BOTTOMLEFT', 3, 1)
    self.power_text:SetText('0')
    self.power_display_visible = false

    self.upper_text = self:createOverlayText(name..'UpperText', 'TOP')
    self.upper_text:ClearAllPoints()
    self.upper_text:SetPoint('TOPLEFT', self.main_icon, 'TOPLEFT', 1, -1)
    self.upper_text_visible = false

    self:applyPosition()
end
----------------------------------------
function Rotor:applyPosition()
    self.anchor:SetPoint('CENTER', UIParent, 'CENTER', self.cfg.x, self.cfg.y)
end
----------------------------------------
function Rotor:showPowerDisplay(initial_text)
    if not self.power_display_visible then
        self.power_text:SetText(initial_text or '0')
        self.power_text:Show()
        self.power_display_visible = true
    end
end
----------------------------------------
function Rotor:hidePowerDisplay()
    if self.power_display_visible then
        self.power_text:Hide()
        self.power_display_visible = false
    end
end
----------------------------------------
function Rotor:updatePowerDisplay()
    if self.power > 1000 then
        self:setPowerText(math.floor((self.power + 500) / 1000) + 'k')
    else
        self:setPowerText(tostring(self.power))
    end
end
----------------------------------------
local last_power_text = ''
function Rotor:setPowerText(text)
    if last_power_text ~= text then
        self.power_text:SetText(text)
        last_power_text = text
    end
end
----------------------------------------
function Rotor:showUpperText(initial_text)
    if not self.upper_text_visible then
        self.upper_text_visible = true
        self:setUpperText(initial_text or '')
        self.upper_text:Show()
    end
end
----------------------------------------
function Rotor:hideUpperText()
    if self.upper_text_visible then
        self.upper_text_visible = false
        self.upper_text:Hide()
    end
end
----------------------------------------
local last_upper_text = ''
function Rotor:setUpperText(text)
    if text ~= last_upper_text then
        self.upper_text:SetText(text)
        last_upper_text = text
    end
end
----------------------------------------
function Rotor:hide()
    if not self.visible then
        return
    end
    for _, icon in pairs(self.icons) do
        icon:hide()
    end
    self.main_icon:hide()
    self.visible = false
    self:updateBars(0, true)
    self:pluginsDo(function(plugin) plugin:MainInterfaceVisibilityChanged(self.visible) end)
end
----------------------------------------
function Rotor:show()
    if self.visible then
        return
    end
    self.main_icon:show()
    self.visible = true
    self:updateBars(0, true)
    self:pluginsDo(function(plugin) plugin:MainInterfaceVisibilityChanged(self.visible) end)
end
----------------------------------------
function Rotor:setInterruptSpell(spell)
    if not spell then
        return
    end

    if type(spell) == 'number' then
        spell = self:RegisterOffGcdSpell(spell)
    end
    local icon = self:getIconAtPos(0)
    icon:initForSpell(spell, 'reminder')
    icon.check_enabled = function() return Rotor.target_casting_interruptible_spell end
    self.track_interruptible_target_spells = true
end
----------------------------------------
--
-- API functions
--

-- HasGlyph(spellid):
--
-- Check if player has the glyph spellid. Returns true if glyph is found,
-- false otherwise.
--
function Rotor:HasGlyph(id)
    return self.glyphs[id]
end

--
-- GetItemId(slot):
--
function Rotor:GetItemId(x)
    local l = GetInventoryItemLink('player', x)

    if l == nil then
        return 0
    end

    local _, ret = string.split(':', l)
    return tonumber(ret)
end

--
-- GetSetItemSlotItems():
--
-- Returns the item ids of head, shoulder, chest, legs and gloves
-- item slot(in that order).
--
function Rotor:GetSetItemSlotItems()
    -- head, shoulder, chest, legs, gloves
    return self:GetItemId(1), self:GetItemId(3), self:GetItemId(5), self:GetItemId(7), self:GetItemId(10)
end

--
-- HasSpell(spell):
--
-- find spell in players spellbook. Return true when found,
-- false otherwise.
--
function Rotor:HasSpell(spell)
    if not spell then
        error('INVALID SPELL (nil)')
    end

    local result = false

    if type(spell) == 'number' then
        result = IsSpellKnown(spell)
    else
        for t = 1, 4 do
            local _, _, o, n = GetSpellTabInfo(t)

            for i = (1 + o), (o + n) do
                local s = GetSpellBookItemName(i, BOOKTYPE_SPELL)

                if (s == spell) then
                    result = true
                    break
                end
            end
        end
    end

    if not result and not self.silent then
        local spell_name
        if type(spell) == 'number' then
            spell_name = GetSpellInfo(spell) or spell
        else
            spell_name = spell
        end
        self:print('HasSpell for "' .. tostring(spell_name) .. '" failed.')
    end

    return result
end

--
-- GetSpellCost(spell_id_or_name)
--
function Rotor:GetSpellCost(spell)
    if type(spell) == 'number' then
        spell = self.spell_name_cache[spell]
    end
    local _, _, _, cost = GetSpellInfo(spell)
    return cost
end

--
-- Auras
--
local Aura = {
    count = 0,
    time_left = 0,
    active = false,
    ends = 0,
    name = '',
    unit = 'player',
    filter = 'HELPFUL',
    texture_name = Rotor.default_texture,
}
----------------------------------------
function Aura:new(name, unit, is_debuff, from_others)
    local aura = {}

    for k, v in pairs(Aura) do
        if k[1] ~= '_' then
            aura[k] = v
        end
    end

    aura.name = name
    aura.unit = unit
    aura.texture_name = Rotor.spell_icon_cache[name]

    if is_debuff then
        aura.filter = 'HARMFUL'
    else
        aura.filter = 'HELPFUL'
    end

    if not from_others then
        aura.filter = aura.filter .. '|PLAYER'
    end

    return aura
end
----------------------------------------
function Aura:isPlayerAura()
    return self.unit == 'player'
end
----------------------------------------
function Aura:isPetAura()
    return self.unit == 'pet'
end
----------------------------------------
function Aura:isPlayerOrPetAura()
    return self:isPlayerAura() or self:isPetAura()
end
----------------------------------------
function Aura:isTargetAura()
    return self.unit == 'target'
end
----------------------------------------
function Aura:update()
    if self.active and (self.duration > 0) and (self.ends < now) then
        self.active = false
        self.time_left = 0
        self.start = 0
        self.ends = 0
        self.duration = 0
        self.count = 0
    end
end
----------------------------------------
function Aura:scan()
    local n, _, tex, cn, _, d, e = UnitAura(self.unit, self.name, nil, self.filter)
    local ctime = GetTime()
    local changed = false

    if not n or ((e <= ctime) and (d ~= 0)) then
        if self.active then
            self.active = false
            self.ends = 0
            self.count = 0
            self.time_left = 0
            changed = true
        end
    else
        changed = not self.active
            or (self.duration ~= d)
            or (self.count ~= cn)
            or (self.texture_name ~= tex)
        self.active = true
        self.duration = d
        self.texture_name = tex
        self.count = cn
        if d == 0 then
            -- endless aura
            changed = (self.time_left ~= 0) or (self.ends ~= 0) or changed
            self.time_left = 0
            self.ends = 0
        else
            self.chaned = (self.ends ~= e) or (self.time_left ~= (e - ctime)) or changed
            self.ends = e
            self.time_left = timeval(e - ctime)
        end
    end

    return changed
end
----------------------------------------
function Rotor:scanAuras(unit)
    local changed = false

    for _, aura in pairs(self.auras) do
        if not unit or aura.unit == unit then
            changed = aura:scan() or changed
        end
    end

    last_aura_scan_time = GetTime()

    return changed
end
----------------------------------------
function Rotor:updateAuras()
    for _, aura in pairs(self.auras) do
        aura:update()
    end
end
----------------------------------------
function Rotor:addAuraIcon(icon_pos, aura, behavior)
    self:getIconAtPos(icon_pos):addAura(aura, behavior)
end
----------------------------------------
function Rotor:registerAura(unit, spell, from_others, is_debuff, icon_pos, icon_behavior)
    if type(spell) == 'number' then
        spell = self.spell_name_cache[spell]
    end

    for _, aura in pairs(self.auras) do
        if (aura.name == spell) and (aura.unit == unit) then
            error('Aura "'..spell..'" already registered for "'..unit..'"')
        end
    end

    local aura = Aura:new(spell, unit, is_debuff, from_others)

    table.insert(self.auras, aura)

    if icon_pos then
        self:addAuraIcon(icon_pos, aura, icon_behavior)
    end

    return aura
end

--
-- RegisterBuff(spell):
--
-- Add a buff to the buffs table.
-- returns aura record
function Rotor:RegisterBuff(spell, from_others, icon_pos, icon_behavior)
    return self:registerAura('player', spell, from_others, false, icon_pos, icon_behavior)
end

function Rotor:RegisterSelfBuff(spell, icon_pos, icon_behavior)
    return self:RegisterBuff(spell, false, icon_pos, icon_behavior)
end

--
-- RegisterPetBuff
--
function Rotor:RegisterPetBuff(spell, from_others, icon_pos, icon_behavior)
    return self:registerAura('pet', spell, from_others, false, icon_pos, icon_behavior)
end

--
-- RegisterDebuff(spell [, mine]):
--
-- Add a debuff to the debuffs table. If mine is true,
-- only check for debuff cast by us.
--
-- returns debuff rec
function Rotor:RegisterDebuff(spell, mine, icon_pos, icon_behavior)
    return self:registerAura('target', spell, not mine, true, icon_pos, icon_behavior)
end

--
-- RegisterPetDebuff
--
function Rotor:RegisterPetDebuff(spell, icon_pos, icon_behavior)
    return self:registerAura('pet', spell, true, true, icon_pos, icon_behavior)
end

----------------------------------------
--
-- Spell functions
--

----------------------------------------
--
-- Spell class
--
local Spell = {
    name = '',
    id = 0,
    texture_name = '',
    cd = 600,
    usable = false,
    cooldown_start = 0,
    cooldown_duration = 0,
    ends = 0,
    sentAt = 0, -- time of UNIT_SPELLCAST_SENT
    doneAt = 0, -- time of UNIT_SPELLCAST_SUCCEEDED
    aura_rec = nil,
    is_pet_spell = false,
    off_gcd = false,
    def = nil,
}
----------------------------------------
local SpellDef = {
    id = 0,
    icon_pos = 0,
    icon_behavior = 'default',
    off_gcd = false,
    with_buff = false,
    with_debuff = false,
    must_exists = false,
    is_pet_spell = false,
    range_check = false,
    min_power = 0,
    min_combo_points = 0,
}
----------------------------------------
function Spell:new(spell_id, extra)
    local spell = {}

    -- clone Spell prototype
    for k, v in pairs(Spell) do
        spell[k] = v
    end

    spell.name = Rotor.spell_name_cache[spell_id]
    spell.id = spell_id
    spell.texture_name = Rotor.spell_icon_cache[spell_id]

    if extra then
        for k, v in pairs(extra) do
            spell[k] = v
        end
    end

    return spell
end
----------------------------------------
function Spell:isUnsent(time_since_last_send)
    return (self.sentAt <= self.doneAt) and (self.doneAt + (time_since_last_send or 0.5) < now)
end
----------------------------------------
function Spell:isOffCdAndUnsent(time_since_last_send, wth)
    return ((self.cd - (wth or 0.4)) <= gcd) and self:isUnsent(time_since_last_send)
end
----------------------------------------
function Rotor:addSpellIcon(icon_pos, spell, behavior)
    self:getIconAtPos(icon_pos):initForSpell(spell, behavior)
end


-- RegisterSpell
-- registers spell within Rotor for tracking colldowns, usablity etc
-- returns spell struct
function Rotor:RegisterSpell(spell_id, icon_pos, icon_behavior, spell_aura, extra)
    if self.spells[spell_id] then
        error('Spell "'..self.spells[spell_id].name..'" already registered')
    end

    local spell = Spell:new(spell_id, extra)

    spell.aura = spell_aura
    self.spells[spell_id] = spell

    if icon_pos then
        self:addSpellIcon(icon_pos, spell, icon_behavior)
    end

    return spell, spell.aura
end
----------------------------------------
function Rotor:RegisterOffGcdSpell(spell_id, icon_pos, icon_behavior, spell_aura)
    return self:RegisterSpell(spell_id, icon_pos, icon_behavior, spell_aura, {off_gcd = true})
end
----------------------------------------
function Rotor:RegisterOffGcdSpellWithSelfBuff(spell_id, icon_pos, icon_behavior)
    return self:RegisterOffGcdSpell(spell_id, icon_pos, icon_behavior, Rotor:RegisterSelfBuff(spell_id))
end
----------------------------------------
function Rotor:RegisterSpellWithSelfBuff(spell_id, icon_pos, icon_behavior)
    return self:RegisterSpell(spell_id, icon_pos, icon_behavior, Rotor:RegisterSelfBuff(spell_id))
end
----------------------------------------
function Rotor:RegisterSpells(spell_defs, spell_table, buff_table, debuff_table)
    -- see SpellDef class for description of spell_defs elements
    -- id of spell can be specified instead of spell def. it is trnasformed to spell def {id = SPECIFIED_ID}
    if spell_defs then
        for spell_mnem, spell_def_or_id in pairs(spell_defs) do
            local spell_def

            if type(spell_def_or_id) == 'number' then
                spell_def = {id = spell_def_or_id}
            else
                spell_def = spell_def_or_id
            end

            local id

            id = spell_def.id
            if not id then
                id = spell_def[1]
            end

            if not id then
                error('no spell id for '..tostring(spell_mnem))
            end

            if spell_def.is_pet_spell or self:HasSpell(id) then
                local aura

                if spell_def.with_buff then
                    aura = self:RegisterSelfBuff(id)
                elseif spell_def.with_debuff then
                    aura = self:RegisterDebuff(id, true)
                end

                local spell = self:RegisterSpell(
                    id,
                    spell_def.icon_pos,
                    spell_def.icon_behavior or spell_def.icon_type,
                    aura)

                spell.def = spell_def
                spell.off_gcd = spell_def.off_gcd
                spell.is_pet_spell = spell_def.is_pet_spell
                spell.min_power = spell_def.min_power
                spell.range_check = spell_def.range_check
                spell.min_combo_points = spell_def.min_combo_points

                if type(spell_mnem) == 'string' then
                    spell_table = spell_table or {}
                    spell_table[spell_mnem] = spell
                    if spell_def.with_buff then
                        buff_table = buff_table or {}
                        buff_table[spell_mnem] = aura
                    elseif spell_def.with_debuff then
                        debuff_table = debuff_table or {}
                        debuff_table[spell_mnem] = aura
                    end
                end
            end
        end
    end

    return spell_table, buff_table, debuff_table
end
----------------------------------------
function Rotor:RegisterAuras(aura_defs, buff_table, debuff_table)
--[[
    aura_def:
        aura_mnemonic={id=AURA_ID, is_debuff=IS_DEBUFF, from_others=FROM_OTHERS, is_pet_aura=IS_PET_AURA, icon_pos=ICON_POS, icon_behavior=ICON_BEHAVIOR}
    returns table of buffs and table of debuffs indexed by mnemonics
--]]
    if aura_defs then
        for aura_mnem, aura_def_or_id in pairs(aura_defs) do
            local unit
            local aura_def

            if type(aura_def_or_id) == 'number' then
                aura_def = {id = aura_def_or_id}
            else
                aura_def = aura_def_or_id
            end

            local id = aura_def.id
            if not id then
                id = aura_def[1] -- one unnamed field
            end
            if not id then
                error('no aura id for '..tostring(aura_mnem))
            end

            unit = aura_def.unit
            if not unit then
                if aura_def.is_pet_buff then
                    unit = 'pet'
                elseif aura_def.is_debuff then
                    unit = 'target'
                else
                    unit = 'player'
                end
            end
            local aura = self:registerAura(unit, id, aura_def.from_others, aura_def.is_debuff, aura_def.icon_pos, (aura_def.icon_behavior or aura_def.icon_type))
            if type(aura_mnem) == 'string' then
                if aura_def.is_debuff then
                    debuff_table = debuff_table or {}
                    debuff_table[aura_mnem] = aura
                else
                    buff_table = buff_table or {}
                    buff_table[aura_mnem] = aura
                end
            end
        end
    end

    return buff_table, debuff_table
end
----------------------------------------
function Rotor:RegisterSpellsAndAuras(spellAndAuraDefs)
    local spell_table, buff_table, debuff_table

    for _, defs in pairs(spellAndAuraDefs) do
        if (defs.spec == self.spec) or ((defs.spec or 0) < 1) then
            spell_table, buff_table, debuff_table = self:RegisterSpells(defs.spells, spell_table, buff_table, debuff_table)
            buff_table, debuff_table = self:RegisterAuras(defs.auras, buff_table, debuff_table)
        end
    end

    return spell_table, buff_table, debuff_table
end
----------------------------------------
function Rotor:pluginsDo(func, ...)
    if self.plugins then
        for _, plugin in pairs(self.plugins) do
            func(plugin, ...)
        end
    end
end

----------------------------------------
--
-- Current module finalization
--
function Rotor:finalizeCurrentModule()
    if not self.cur_mod then
        -- already finalized
        return
    end

    local old_mod = self.cur_mod

    self:unregisterEvents()

    self:hidePowerDisplay()

    self:hideUpperText()

    self:hide()

    self.cur_mod = nil

    self.target_guid = nil
    self.target_is_player = false

    self.track_interruptible_target_spells = false
    self.target_casting_interruptible_spell = false

    if old_mod then
        old_mod:Done()
    end

    self:initIcons()

    self.auras = {}
    self.spells = {}

    self:pluginsDo(function(plugin) plugin:CurrentModuleFinalized() end)
end

--
-- chooseModule():
--
-- select module for the current spec.
--
local function isSameGlyphs(g1, g2)
    for id, _ in pairs(g1) do
        if not g2[id] then
            return false
        end
    end
    for id, _ in pairs(g2) do
        if not g1[id] then
            return false
        end
    end
    return true
end
----------------------------------------
function Rotor:startProfiling()
    self.profile_start_time = debugprofilestop()
    self.time_used = 0
end
----------------------------------------
function Rotor:incTimeUsed(since)
    self.time_used = self.time_used + (debugprofilestop() - since)
end
----------------------------------------
function Rotor:stopProfiling()
    self.total_profile_time = self.total_profile_time + (debugprofilestop() - self.profile_start_time)
    self.total_time_used = self.total_time_used + self.time_used
    self.profile_start_time = 0
end
----------------------------------------
function Rotor:printProfile()
    local profiling = self.profile_start_time ~= 0
    if profiling then
        self:stopProfiling()
    end
    print(format('time used: %d (%.3f%%)', self.total_time_used, self.total_time_used * 100 / self.total_profile_time))
    print(format('time total: %d', self.total_profile_time))
    if profiling then
        self:startProfiling()
    end
end
----------------------------------------
function Rotor:chooseModule()
    local tal_tbl, tal_sig = getTalents()
    local glyphs = getGlyphs()
    local old_mod = self.cur_mod

    if self.cur_mod
        and (self.spec == (GetPrimaryTalentTree() or 0))
        and (self.talents_sig == tal_sig)
        and isSameGlyphs(glyphs, self.glyphs)
    then
        return
    end

    self.spec = GetPrimaryTalentTree() or 0
    self.player_level = UnitLevel('player')
    self.talents = tal_tbl
    self.talents_sig = tal_sig
    self.glyphs = glyphs

    local new_mod = nil
    for _, rmod in pairs(self.modules) do
        if rmod:Detect() then
            new_mod = rmod
            break
        end
    end

    self:finalizeCurrentModule()

    if not new_mod then
        self:print(L['No module found for current spec/class.'])
        return
    end

    local s
    local m

    s, m = new_mod:Init(createOptTable(new_mod:Name()))

    if not s then
        self:print(string.format(L['Error: init failed for module \':%s\' (Reason: %s)'], new_mod:Name(), m or 'none given'))
        return
    end

    self.upper_text:SetText('')

    self.cur_mod = new_mod
    self:printOptInfo(string.format(L['Using module %s.'], self.cur_mod:Name()))

    self:registerEvents()

    self:pluginsDo(function(plugin) plugin:NewModuleSelected() end)

    self:checkTarget()
end

----------------------------------------
--
-- Events
--
local Events = {
    'PLAYER_TARGET_CHANGED',
    'PLAYER_ENTERING_WORLD',
    'PLAYER_REGEN_ENABLED',
    'PLAYER_REGEN_DISABLED',
    'UNIT_INVENTORY_CHANGED',
    'PARTY_MEMBERS_CHANGED',
    'UNIT_FACTION'
}
----------------------------------------
local EventsWithValidTarget = {
    'UNIT_SPELLCAST_SENT',
    'UNIT_SPELLCAST_SUCCEEDED',
    'UNIT_SPELLCAST_FAILED',
    'UNIT_SPELLCAST_FAILED_QUIET',
    'UNIT_SPELLCAST_INTERRUPTED',
    'UNIT_SPELLCAST_START',
    'UNIT_SPELLCAST_STOP',
    'UNIT_PET',
    'UNIT_AURA',
    'SPELL_UPDATE_COOLDOWN',
    'ACTIONBAR_UPDATE_COOLDOWN',
    'SPELL_UPDATE_USABLE',
}
----------------------------------------
function Rotor:registerEvents()
    for _, event in pairs(Events) do
        self:registerEvent(event)
    end
end
----------------------------------------
function Rotor:unregisterEvents()
    for _, event in pairs(Events) do
        self:unregisterEvent(event)
    end
    self:unregisterEventsWithValidTarget()
    if self.track_interruptible_target_spells then
        self:unregisterEvent('UNIT_SPELLCAST_CHANNEL_START')
        self:unregisterEvent('UNIT_SPELLCAST_CHANNEL_STOP')
        self:unregisterEvent('UNIT_SPELLCAST_CHANNEL_INTERRUPTED')
        self:unregisterEvent('UNIT_SPELLCAST_NOT_INTERRUPTIBLE')
        self:unregisterEvent('UNIT_SPELLCAST_INTERRUPTIBLE')
    end
end
----------------------------------------
function Rotor:registerEventsWithValidTarget()
    for _, event in pairs(EventsWithValidTarget) do
        self:registerEvent(event)
    end
    if self.cur_mod.HandleUnfilteredCombatEvent then
        self:registerEvent('COMBAT_LOG_EVENT_UNFILTERED')
    end
    if self.track_interruptible_target_spells then
        self:registerEvent('UNIT_SPELLCAST_CHANNEL_START')
        self:registerEvent('UNIT_SPELLCAST_CHANNEL_STOP')
        self:registerEvent('UNIT_SPELLCAST_CHANNEL_INTERRUPTED')
        self:registerEvent('UNIT_SPELLCAST_NOT_INTERRUPTIBLE')
        self:registerEvent('UNIT_SPELLCAST_INTERRUPTIBLE')
    end
end
----------------------------------------
function Rotor:unregisterEventsWithValidTarget()
    for _, event in pairs(EventsWithValidTarget) do
        self:unregisterEvent(event)
    end
    self:unregisterEvent('COMBAT_LOG_EVENT_UNFILTERED')
end
----------------------------------------
function Rotor:isTargetCastingInterruptibleSpell()
    local name, _, _, _, _, _, _, _, notInterruptible = UnitCastingInfo('target')
    if name then
        return not notInterruptible
    else
        name, _, _, _, _, _, _, notInterruptible = UnitChannelInfo('target')
        if name then
            return not notInterruptible
        end
    end

    return false
end
----------------------------------------
function Rotor:targetAcquired(new_guid)
    if not self.on_target then
        self:startProfiling()
    end

    local old_target = self.target_guid
    self.target_guid = new_guid
    self.target_is_player = UnitIsPlayer('target')
    self.target_level = UnitLevel('target')
    self.on_target = true

    self.last_hp_probe_time = GetTime()
    self.hp_probes:clear()
    self.hp_probes:push(UnitHealth('target'))
    self.cur_dps = 0

    cur_spell = -1
    wait_for_send_till = 0
    last_gcd = 0

    if not old_target then
        self:registerEventsWithValidTarget()
    end

    self.cur_mod:TargetChanged(old_target)

    if self.track_interruptible_target_spells then
        self.target_casting_interruptible_spell = self:isTargetCastingInterruptibleSpell()
    end

    last_aura_scan_time = 0 -- force full scan of auras
    self:forceUpdate(0)
    self:show()
    self:pluginsDo(function(plugin) plugin:TargetChanged(old_target) end)
end
----------------------------------------
function Rotor:targetLost()
    if self.target_guid then
        local old_target = self.target_guid
        self.target_guid = nil
        self.on_target = false
        self:unregisterEventsWithValidTarget()
        self:hide()
        self.cur_mod:TargetChanged(old_target)
        self:pluginsDo(function(plugin) plugin:TargetChanged(old_target) end)
        self:stopProfiling()
    end
end
----------------------------------------
function Rotor:checkTarget()
    local n = UnitName('target')
    local guid = UnitGUID('target')
    local is_dead = UnitIsDead('target')

    next_target_check = 0

    if (n == nil)
        or not UnitCanAttack('player', 'target')
        or is_dead
        or (self.player_in_pvp and not self.cfg.showinpvp)
        or (self.player_in_raid and not self.cfg.showinraid)
        or (not self.player_in_raid and self.player_in_party and not self.cfg.showinparty)
        or (not self.player_in_raid and not self.player_in_party and not self.cfg.showwhensolo)
        or (not self.cfg.showinvehicles and UnitInVehicle('player'))
        or (self.boss_only and (UnitLevel('target') ~= -1))
    then
        self:targetLost()
        if n and not is_dead then
            next_target_check = GetTime() + 1 -- protection against missing UNIT_FACTION (e.g. morchok in ds)
        end
        return
    end

    if guid == self.target_guid then
        return
    end

    self:targetAcquired(guid)
end
----------------------------------------
function Rotor:CheckInRange(spell_id)
    if not self.cur_mod then
        return self:defaultRangeCheck(spell_id)
    else
        return self.cur_mod:CheckInRange(spell_id)
    end
end
----------------------------------------
function Rotor:spellcastFailed(spell, reason)
    local rec = self.spells[self.spell_id_cache[spell]]
    if rec then
        rec.sentAt = 0 -- fail send
        rec.doneAt = 0
    end

    if not self.cur_mod:CastFinished(spell, false, reason, rec) then
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:queueCurrentModuleUpdate(force, reason)
    if force then
        self:finalizeCurrentModule()
    end
    self.module_switch_time = GetTime() + 1
end
----------------------------------------
function Rotor:checkPet()
    self.pet_exists = UnitExists('pet')
    self.pet_alive = self.pet_exists and not UnitIsDead('pet')
end
----------------------------------------
function Rotor:ttl()
    if not self.on_target or (self.cur_dps < 0.1) then
        return nil
    else
        return math.floor(UnitHealth('target') / self.cur_dps)
    end
end

----------------------------------------
--
-- Events
--
----------------------------------------
function Rotor:PLAYER_REGEN_ENABLED()
    self.in_combat = false
    self.cur_mod:CombatStateChanged()
    self:pluginsDo(function(plugin) plugin:CombatStateChanged() end)
    if self.on_target then
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:PLAYER_REGEN_DISABLED()
    self.in_combat = true
    self.cur_mod:CombatStateChanged()
    if self.target_guid then
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:PLAYER_TARGET_CHANGED()
    self:checkTarget()
end
----------------------------------------
function Rotor:UNIT_FACTION(unit)
    if unit == 'target' then
        self:checkTarget()
    end
end
----------------------------------------
function Rotor:PLAYER_ENTERING_WORLD()
    local _, t = GetInstanceInfo()
    if t == 'pvp' then
        self.player_in_pvp = true
    else
        self.player_in_pvp = false
    end

    self:checkTarget()
end
----------------------------------------
function Rotor:PARTY_MEMBERS_CHANGED()
    self.player_in_raid = GetNumRaidMembers() > 0
    self.player_in_party = GetNumPartyMembers() > 0

    self:checkTarget()
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_SENT(caster, spell)
    if caster == 'player' then
        local rec = self.spells[self.spell_id_cache[spell]]

        if rec then
            rec.sentAt = GetTime()
        end

        self.cur_mod:CastSent(spell, rec)
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_SUCCEEDED(caster, spell)
    if caster == 'player' then
        local rec = self.spells[self.spell_id_cache[spell]]
        if rec then
            rec.doneAt = GetTime()
        end

        if not self.cur_mod:CastFinished(spell, true, nil, rec) then
            self:forceUpdate()
        end
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_FAILED(caster, spell)
    if caster == 'player' then
        self:spellcastFailed(spell, 'FAILED')
    elseif self.target_casting_interruptible_spell and (caster == 'target') then
        self.target_casting_interruptible_spell = false
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_FAILED_QUIET(caster, spell)
    if caster == 'player' then
        self:spellcastFailed(spell, 'FAILED_QUIET')
    elseif self.target_casting_interruptible_spell and (caster == 'target') then
        self.target_casting_interruptible_spell = false
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_INTERRUPTED(caster, spell)
    if caster == 'player' then
        self:spellcastFailed(spell, 'INTERRUPTED')
    elseif self.target_casting_interruptible_spell and (caster == 'target') then
        self.target_casting_interruptible_spell = false
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_CHANNEL_INTERRUPTED(caster)
    if self.target_casting_interruptible_spell and (caster == 'target') then
        self.target_casting_interruptible_spell = false
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_START(caster)
    if caster == 'player' then
        local _, _, _, _, startTime, endTime = UnitCastingInfo('player')
        self.spellcast_start_time = startTime / 1000.0
        self.spellcast_end_time = endTime / 1000.0
        self:forceUpdate()
    elseif self.track_interruptible_target_spells and (caster == 'target') then
        local _, _, _, _, _, _, _, _, notInterruptible = UnitCastingInfo('target')
        self.target_casting_interruptible_spell = not notInterruptible
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_STOP(caster)
    if caster == 'player' then
        if self.spellcast_start_time < self.spellcast_end_time then
            self.spellcast_end_time = 0
            self:forceUpdate()
        end
    elseif self.target_casting_interruptible_spell and (caster == 'target') then
        self.target_casting_interruptible_spell = false
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_CHANNEL_START(caster)
    if self.track_interruptible_target_spells and (caster == 'target') then
        local _, _, _, _, _, _, _, notInterruptible = UnitChannelInfo('target')
        self.target_casting_interruptible_spell = not notInterruptible
        if self.target_casting_interruptible_spell then
            self:forceUpdate()
        end
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_CHANNEL_STOP(caster)
    if (caster == 'target') and self.target_casting_interruptible_spell then
        self.target_casting_interruptible_spell = false
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_NOT_INTERRUPTIBLE(unit)
    if (unit == 'target') then
        if self.target_casting_interruptible_spell then
            self.target_casting_interruptible_spell = false
            self:forceUpdate()
        end
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_INTERRUPTIBLE(unit)
    if (unit == 'target') then
        if not self.target_casting_interruptible_spell then
            self.target_casting_interruptible_spell = true
            self:forceUpdate()
        end
    end
end
----------------------------------------
function Rotor:PLAYER_TALENT_UPDATE()
    self:queueCurrentModuleUpdate(false, 'PLAYER_TALENT_UPDATE')
end
----------------------------------------
function Rotor:ACTIVE_TALENT_GROUP_CHANGED()
    self:queueCurrentModuleUpdate(false, 'ACTIVE_TALENT_GROUP_CHANGED')
end
----------------------------------------
function Rotor:UNIT_PET(unit)
    if unit == 'player' then
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_INVENTORY_CHANGED(unitID)
    if unitID == 'player' then
        if self.cur_mod then
            self.cur_mod:InventoryChanged()
        end
    end
end
----------------------------------------
function Rotor:GLYPH_ADDED()
    self:queueCurrentModuleUpdate(false, 'GLYPH_ADDED')
end
----------------------------------------
function Rotor:GLYPH_REMOVED()
    self:queueCurrentModuleUpdate(false, 'GLYPH_REMOVED')
end
----------------------------------------
function Rotor:COMBAT_LOG_EVENT_UNFILTERED(...)
    if self.cur_mod:HandleUnfilteredCombatEvent(...) then
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:SPELL_UPDATE_USABLE()
    self:forceUpdate()
end
----------------------------------------
function Rotor:UNIT_AURA(unit)
    if (unit == 'player') or (unit == 'target') or (unit == 'pet') then
        if self:scanAuras(unit) then
            self:forceUpdate()
        end
    end
end
----------------------------------------
function Rotor:ACTIONBAR_UPDATE_COOLDOWN()
    self:forceUpdate()
end
----------------------------------------
function Rotor:SPELL_UPDATE_COOLDOWN()
    self:forceUpdate()
end
----------------------------------------
-- this event is registered in ADDON_LOADED and unregistered
-- once it fired. The reason for this is that on login, the
-- spellbook is not initialized, but after SKILL_LINES_CHANGED
-- fired.
function Rotor:SKILL_LINES_CHANGED()
    self:unregisterEvent('SKILL_LINES_CHANGED')

    -- find suitable spell for gcd_spell
    local reference_spells = {
        1978,   -- Serpent Sting(Hunter)
        47541,  -- Death Coil(Death Knight)
        45902,  -- Blood Strike (Death Knight)
        585,    -- Smite(Priest)
        20154,  -- Seal of Righteousness (Paladin)
        172,    -- Corruption(Warlock)
        42956,  -- Conjure Refreshment (Mage)
        772,    -- Rend(Warrior)
        94009,  -- Rend (Warrior)
        331,    -- Healing Wave(Shaman)
        1752,   -- Sinister Strike(Rogue)
        5176,   -- Wrath(Druid)
    }

    for _, spell_id in pairs(reference_spells) do
        if IsSpellKnown(spell_id) then
            self.gcd_spell = spell_id
            break
        end
    end

    if self.gcd_spell == nil then
        self:print(L['Error: didn\'t find a reference spell for the global coldown, terminated.'])
        return
    end

    self:registerEvent('PLAYER_TALENT_UPDATE')
    self:registerEvent('ACTIVE_TALENT_GROUP_CHANGED')
    self:registerEvent('GLYPH_ADDED')
    self:registerEvent('GLYPH_REMOVED')

    self:queueCurrentModuleUpdate(false, 'SKILL_LINES_CHANGED')
end
----------------------------------------
function Rotor:ADDON_LOADED(addon)
    if addon ~= ADDON_NAME then
        return nil
    end

    self:unregisterEvent('ADDON_LOADED')

    self:initialize()
end
----------------------------------------
Rotor:registerEvent('ADDON_LOADED')
